Skip to content

BREAKING: stop publishing to Docker Hub (GHCR only) + migrate build to shared bake reusable - #45

Merged
CybotTM merged 2 commits into
masterfrom
ci/bake-migration
Jul 22, 2026
Merged

BREAKING: stop publishing to Docker Hub (GHCR only) + migrate build to shared bake reusable#45
CybotTM merged 2 commits into
masterfrom
ci/bake-migration

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 22, 2026

Copy link
Copy Markdown
Member

⚠️ Breaking: Docker Hub publishing stops

docker.io/netresearch/docker-mariadb will no longer receive updates.

This repo currently pushes the same digest to both ghcr.io/netresearch/docker-mariadb
and docker.io/netresearch/docker-mariadb. The Docker Hub repository is public and has
18,725 pulls (verified via the Docker Hub API today; its last_updated was this
morning's nightly run). This PR drops the docker.io image name and the second
docker/login-action, so:

  • Everyone doing docker pull netresearch/docker-mariadb (:latest, :master, :nightly)
    keeps getting the image published before this PR merges, frozen forever — no more
    base-image refreshes, no more OS security patches.
  • All future builds land on ghcr.io/netresearch/docker-mariadb only.
  • This is user-visible and effectively irreversible for consumers who never notice:
    nothing errors, the image just silently stops ageing.

This is an explicit owner decision, not a side effect of the migration. It also happens to
be the only shape the shared reusable supports — build-container-bake.yml logs into
exactly one registry (registry: input), so a dual-registry push is not reproducible
through it.

Recommended follow-up (deliberately NOT done in this PR)

  1. Mark the Docker Hub repository deprecated: push a final README/overview to
    netresearch/docker-mariadb on Docker Hub saying the image is unmaintained and pointing
    at ghcr.io/netresearch/docker-mariadb. Nothing in this repo pushes the Hub description,
    so this is a manual step in the Docker Hub UI.
  2. Delete the now-unused DOCKER_USERNAME / DOCKER_PASSWORD repository secrets.
  3. Consider announcing the move before merging — ~18.7k pulls implies real consumers.

What else changed: zero step-level actions

docker-publish.yml had 7 step-level uses: (checkout, cosign-installer, setup-buildx,
2× login-action, metadata-action, build-push-action). It now has none — one job calls
netresearch/.github/.github/workflows/build-container-bake.yml@main, and the build is
described by a new docker-bake.hcl.

Before / after

Behaviour Before After Same?
Registries pushed ghcr.io and docker.io ghcr.io only intentional (above)
GHCR tags, push to master master, latest master, latest ✅ verified
GHCR tags, schedule nightly, master, latest nightly, master, latest ✅ verified
GHCR tags, pull_request (built, not pushed) pr-N, latest pr-N, latest ✅ verified
OCI labels incl. org.opencontainers.image.version metadata-action defaults identical (master/nightly/pr-N) ✅ verified
Push on PR no no
Signing mechanism cosign keyless OIDC (COSIGN_EXPERIMENTAL=true, id-token: write, cosign sign --yes …@digest, no --key) cosign keyless OIDC via sign: true
Signed reference the pushed digest, once every tag from bake --print (:master, :latest) ⚠️ differs, see below
Build platform runner default (linux/amd64; no platforms: set) unchanged (no platform in HCL or workflow)
Provenance / SBOM buildx defaults unchanged — attest/sbom left at reusable default false
Layer cache none GHA cache on push/PR, off on schedule ⚠️ new, see below
Trivy scan none none (scan: false)
Job permissions contents:read, packages:write, id-token:write + security-events: write ⚠️ required, see below
Triggers schedule 16 14 * * *, push/PR on master unchanged

Intentional deltas, spelled out

  1. Docker Hub dropped — see the top section.
  2. Signing target: tags instead of digest. The reusable enumerates tags from
    docker buildx bake --print and runs cosign sign --yes <tag> for each. Cosign resolves
    the tag to its digest and stores the signature at the digest, so the end state is
    equivalent — but it is a tag→digest resolution after the push rather than the digest
    the build returned, and it signs the same digest twice (once via :master, once via
    :latest). Harmless here, but it is not literally the old digest-pinned command.
  3. Layer cache is new. Today there is no cache at all — every build is a full rebuild.
    The reusable defaults cache: true; this PR passes
    cache: ${{ github.event_name != 'schedule' }}. Rationale: the nightly run exists to pull
    a fresh FROM mariadb (unpinned floating tag) and re-run the apt-get upgrade layer —
    with cache-from it would restore the old layer chain and rebuild nothing, silently
    turning that security refresh into a no-op. Push/PR runs keep the cache for speed.
    There is no workflow_dispatch trigger here, so nothing to special-case for it; if one
    is added later it should be treated like schedule.
    For strict parity instead, set cache: false — push builds would then also always
    re-resolve FROM mariadb.
  4. security-events: write added to the calling job. A called workflow's job
    permissions are validated statically at workflow startup, before any if: — the caller
    must grant the union of everything build-container-bake.yml declares, or the run fails
    with startup_failure. Granted even though scan: false.
  5. scan: false. This workflow never scanned, and container scanning already runs in
    ci.yml (docker-image-ci.ymlbuild-container.yml, scan: true) on push and PR.
    Flip to true if you also want the nightly rebuild scanned — the permission is in place.
  6. Two unused build args appear. metadata-action's tag bake file injects
    DOCKER_META_IMAGES / DOCKER_META_VERSION as build args; the Dockerfile does not
    declare them, so BuildKit emits an "unused build arg" warning. Cosmetic.

Explicitly not added

  • GitHub-issued SLSA attestation. attest: true on the bake reusable is buildx
    provenance=mode=max (in-image BuildKit attestation) — a different mechanism from
    actions/attest-build-provenance. This repo has neither today, so neither was added.
    If gh attestation verify-able provenance is wanted, add a second job calling
    netresearch/.github/.github/workflows/attest-image.yml@main with the bake-metadata
    output and attestations: write.
  • Multi-arch. Today's build is single-platform by omission; the HCL declares no
    platforms, preserving that. Adding arm64 is a separate decision.
  • README changes. Nothing in the README states where to pull from, so no doc claim
    becomes false. (Unrelated pre-existing observation, not touched: both microbadger badges
    at the top are dead — images.microbadger.com no longer resolves.)

How this was verified

  • Tag/label parity — measured, not reasoned. Ran the pinned docker/metadata-action
    v6.2.0 dist (dc80280…, confirmed via the API to be the v6.2.0 tag object) locally under
    node, with synthesised push / schedule / pull_request contexts for this repo: once
    with the old config (2 images, default tags, flavor: latest=true) and once with the new
    one (ghcr only, explicit metadata-tags). The GHCR tag lists and the version output
    (which drives org.opencontainers.image.version) match exactly for all three events;
    only the docker.io/... lines disappear. type=raw has the lowest priority (200), so
    latest stays a secondary tag exactly as flavor: latest=true produced it.
  • Bake stub inheritance. Fed the actual docker-metadata-action-bake-tags.json and
    -bake-labels.json that run wrote into
    docker buildx bake -f docker-bake.hcl -f <tags> -f <labels> app --print, together with
    the --set *.cache-from/cache-to overrides the reusable applies. The app target
    resolves with the expected tags, labels and cache config.
  • Sign/scan enumeration. Replayed the reusable's jq over that --print output:
    SIGN_TAGS = :master + :latest, SCAN_REFS = :master.
  • The bake file actually builds. docker buildx bake -f docker-bake.hcl app completed
    locally against the current Dockerfile (exit 0, image exported).
  • actionlint .github/workflows/docker-publish.yml → clean, exit 0.
  • The PR's own run. build / Build Container (Bake) on this PR succeeded (no
    startup_failure, so the permission union is right), built the app target, and the
    enumerate step resolved exactly ghcr.io/netresearch/docker-mariadb:pr-45 and
    :latest — matching the locally predicted pull_request tag set. Nothing was pushed
    (push: false on PRs) and no docker.io/... reference appears anywhere in the run.
  • Read in full before writing: the old workflow, build-container-bake.yml,
    attest-image.yml, and ci.yml (to confirm scanning is already covered).
  • Not verified: yamllint is broken in this environment (missing module), so YAML style
    lint was not run — lint-yaml in ci.yml covers it. The live push + cosign path cannot
    be exercised outside Actions; it is inherited unchanged from the reusable.

Review follow-up (owner decisions)

Base-image pulls become unauthenticated — accepted

Removing the docker.io login also removed authenticated Docker Hub pulls of the
FROM mariadb base image, not just the push. Anonymous pulls are subject to Docker Hub's
stricter anonymous rate limit, so a busy build day can intermittently fail with
toomanyrequests.

The owner has accepted this trade-off rather than expanding the shared reusable with a
pull-registry credentials interface, or mirroring the base image into GHCR. If rate limiting
turns out to bite in practice, those two options remain open — the second is the durable fix.

Credential removal — check before deleting

An org-wide code search finds DOCKER_PASSWORD referenced by this repository only, so the
DOCKER_USERNAME / DOCKER_PASSWORD org secrets look unused after this merge. That search
can miss dynamic references, so confirm before deleting them rather than treating this as
proof.

Two review findings fixed

Finding Fix
latest was emitted twice on a tag push — an explicit metadata-tags list leaves flavor at latest=auto, which appends latest on top of the explicit type=raw,value=latest metadata-flavor: latest=false, using the input added in netresearch/.github#267. Exactly one latest now.
A cached push build could publish a less patched image than the nightly had just published: the nightly rebuilds uncached to pick up apt upgrade, but a following cached push restored the pre-upgrade layer chain and overwrote :latest/:master cache limited to pull_request. Every run that publishes now rebuilds for real.

Not verified here: neither the schedule nor the tag-push path can be exercised from a pull
request, so both changes are established by reading the reusable's Compose bake --set overrides step and metadata-action's flavor handling, not by execution.

BREAKING CHANGE: images are no longer published to Docker Hub.

docker.io/netresearch/docker-mariadb stops receiving updates. The existing
tags stay pullable but frozen; all future builds go to
ghcr.io/netresearch/docker-mariadb only. The reusable build workflow logs
into exactly one registry, and the owner decided to consolidate on GHCR.

Migration of the build itself:

- docker-publish.yml now has zero step-level `uses:`; the single job calls
  netresearch/.github/.github/workflows/build-container-bake.yml@main.
- New docker-bake.hcl drives the build. Tags/labels are not hardcoded: an
  empty `docker-metadata-action` target is inherited by the `app` target and
  populated at build time by the tag/label bake files docker/metadata-action
  generates.
- `bake-source: "."` is required because those generated files live in
  $RUNNER_TEMP on the runner, which the default git-remote bake context
  cannot read.
- `flavor: latest=true` is re-expressed as `type=raw,value=latest`. Verified
  against docker/metadata-action v6.2.0 run locally for push, schedule and
  pull_request: the GHCR tag set and the version output (and therefore the
  org.opencontainers.image.version label) are identical to today.
- `cache: ${{ github.event_name != 'schedule' }}` — the nightly run exists to
  pick up a fresh `FROM mariadb` base image and the apt upgrade layer; with
  the GHA layer cache it would restore the old chain and rebuild nothing.
- `scan: false` keeps parity: this workflow never scanned, and container
  scanning already runs in ci.yml via docker-image-ci.
- Signing stays cosign keyless (OIDC); it now signs each pushed tag instead
  of the single pushed digest.
- The calling job must grant `security-events: write` because a called
  workflow's job permissions are validated statically at startup.

The DOCKER_USERNAME / DOCKER_PASSWORD secrets are now unused.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@github-actions

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
See the Details below.

License Issues

.github/workflows/docker-publish.yml

PackageVersionLicenseIssue Type
netresearch/.github/.github/workflows/build-container-bake.ymlmainNullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
actions/netresearch/.github/.github/workflows/build-container-bake.yml main UnknownUnknown

Scanned Files

  • .github/workflows/docker-publish.yml

CybotTM added a commit to netresearch/.github that referenced this pull request Jul 22, 2026
Exposes `docker/metadata-action`'s `flavor:` as an input.

## Why

Supplying an explicit `metadata-tags` list leaves `flavor` at its
default `latest=auto`, which appends `latest` on a tag push. A caller
whose tag list already carries an explicit `type=raw,value=latest` — the
way to reproduce a previous `flavor: latest=true` — therefore emits
`latest` **twice** on tag pushes. Found while migrating
[netresearch/docker-mariadb#45](netresearch/docker-mariadb#45),
where it was measured by running the pinned action locally against a
synthesised tag-push context.

## Backward compatibility

Default `""`. An unset GitHub Actions input already reads as `""`, so
`flavor: ""` is byte-for-byte what `metadata-action` received before
this change — existing callers are unaffected.

## Validation
- `actionlint` clean
Two review findings.

metadata-flavor: an explicit metadata-tags list leaves metadata-action's
flavor at `latest=auto`, which appends `latest` again on a tag push. The
explicit `type=raw,value=latest` already reproduces the previous
`flavor: latest=true` on every event, so `latest=false` turns the implicit
one off and exactly one `latest` is emitted. Uses the metadata-flavor input
added in netresearch/.github#267.

cache: was `!= 'schedule'`, so a push build was cached. Since the nightly
rebuilds without cache to pick up `apt upgrade`, a subsequent cached push
would restore the pre-upgrade layer chain and overwrite `:latest` /
`:master` with a LESS patched image than the nightly had just published.
Cache is now limited to pull_request, so every run that publishes rebuilds
for real.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@sonarqubecloud

Copy link
Copy Markdown

@CybotTM
CybotTM merged commit 612ddee into master Jul 22, 2026
18 checks passed
@CybotTM
CybotTM deleted the ci/bake-migration branch July 22, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant